Skip to content

feat: ingestão de contribuições do GitHub e retrospectiva da comunidade#304

Open
Clintonrocha98 wants to merge 33 commits into
4.xfrom
feat/community-retrospective
Open

feat: ingestão de contribuições do GitHub e retrospectiva da comunidade#304
Clintonrocha98 wants to merge 33 commits into
4.xfrom
feat/community-retrospective

Conversation

@Clintonrocha98
Copy link
Copy Markdown
Member

@Clintonrocha98 Clintonrocha98 commented Jun 3, 2026

O que este PR faz

image image

Hoje a comunidade não tem um jeito automático de saber quem está contribuindo nos repositórios públicos. Este PR cria essa base: o sistema passa a guardar as contribuições do GitHub (pull requests, reviews, issues, comentários e commits) e a mostrar tudo numa apresentação de retrospectiva — no espírito do "Quem fez a He4rt bater".

Como funciona, em linguagem simples

1. De onde vêm os dados

  • Histórico: com um clique no painel (ou um comando), o sistema busca tudo o que já aconteceu num repositório e guarda — desde o primeiro registro do projeto.
  • Tempo real: quando alguém abre um PR, comenta ou faz um commit, o GitHub avisa o sistema na hora e a contribuição é registrada sozinha.

2. Quais repositórios contam

  • Um administrador escolhe, pelo painel, quais repositórios entram na conta. Adicionar um novo é só cadastrar — sem mexer em código.

3. Quem aparece

  • Todo mundo que contribui é contado, seja membro cadastrado ou não.
  • Robôs (bots) ficam de fora.
  • PRs que não foram aceitos ainda contam como participação, mas aparecem separados dos que foram aceitos — pra dar crédito ao esforço sem confundir com o que de fato entrou.

A apresentação (deck)

A retrospectiva é uma apresentação em tela cheia, navegável por teclado (← →) ou pelas setas/bolinhas do rodapé, sem o "chrome" do portal. A sequência conta uma história: capa → panorama → núcleo → comunidade → repositórios → destaques → encerramento.

  • Capa animada: coração da He4rt pulsando, o logo da He4rt ao fundo com um LED percorrendo as linhas e a linha do batimento (ECG) se desenhando na entrada do slide.
  • Cards de contribuidor: cada pessoa tem uma barra de composição (o "DNA" da participação — a proporção de PRs, reviews, issues, comentários e commits por cor) e os tipos de atividade com ícone ("Abriu PR", "Revisou", "Abriu issue", "Comentou", "Commitou").

Filtros (aplicados ao vivo)

Um painel de filtros controla o que a apresentação mostra, e tudo fica refletido na URL — dá pra compartilhar um recorte já pronto:

  • Período: datas livres ou presets — semana, mês ou tudo (que cobre desde a primeira contribuição real, respeitando os repositórios selecionados).
  • Tipo de contribuição: ligar/desligar PR, review, issue, comentário e commit.
  • Repositórios: filtrar por um ou vários repos.
  • Desfecho de PR: todos, só aceitos (merged), só abertos ou só fechados sem merge.
  • Pessoa: focar numa pessoa específica.
  • Bots: mostrar ou ocultar (ocultos por padrão).
  • Ordenação: por total de interações, por PRs ou por linhas de código.

O que muda pra quem usa

  • Para o admin: uma tela nova ("Repositórios") pra escolher o que entra e um botão "Backfill agora" pra puxar o histórico. Se o GitHub limitar as requisições, aparece um aviso claro pedindo pra tentar de novo depois (e nada é perdido — dá pra continuar de onde parou).
  • Para a comunidade: a apresentação de retrospectiva, filtrável por período e escopo.

Substitui a versão provisória

A primeira tentativa era um comando solto, rodado na mão, que jogava um JSON num link temporário. Isso foi removido e trocado por essa base que vive dentro da arquitetura do projeto, guarda os dados de verdade e se atualiza sozinha.

Para funcionar em produção (configuração)

Precisa configurar duas chaves (GITHUB_API_TOKEN e GITHUB_WEBHOOK_SECRET) e registrar o webhook na organização do GitHub. Sem isso, o histórico pelo painel ainda funciona; só o tempo real fica desligado.

Nota: ainda em draft — falta a configuração das chaves/webhook no ambiente e uma passada final de revisão.

@Clintonrocha98 Clintonrocha98 requested a review from a team June 3, 2026 17:56
Copy link
Copy Markdown

@fernanduandrade fernanduandrade left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(comentário do contexto provisório anterior — removido)

Substitui a retrospectiva provisória (comando que rodava `gh` na mão e subia
um JSON para um link temporário) por uma ingestão persistente que respeita a
arquitetura modular do projeto.

- integration-github: modelo próprio de contribuições (desacoplado de
  Interaction/gamification), lake bruto de eventos espelhando o do Discord,
  backfill REST resiliente e ciente de rate limit (helper RateLimit
  compartilhado), e webhook ao vivo com verificação de assinatura HMAC.
- panel-admin: allowlist de repositórios gerenciável + ação "Backfill agora"
  com notificações amigáveis (rate limit / falha) sem marcar sucesso falso.
- portal: página pública de retrospectiva por período (Livewire), contando
  todos os contribuidores, excluindo bots e distinguindo PRs mergeados dos
  fechados sem merge.

A gamificação fica como seam (evento GithubContributionRecorded), sem
dependência direta entre os módulos.
@he4rt he4rt deleted a comment from coderabbitai Bot Jun 5, 2026
@he4rt he4rt deleted a comment from coderabbitai Bot Jun 5, 2026
@he4rt he4rt deleted a comment from coderabbitai Bot Jun 5, 2026
@he4rt he4rt deleted a comment from coderabbitai Bot Jun 5, 2026
@he4rt he4rt deleted a comment from coderabbitai Bot Jun 5, 2026
@he4rt he4rt deleted a comment from coderabbitai Bot Jun 5, 2026
@he4rt he4rt deleted a comment from coderabbitai Bot Jun 5, 2026
@he4rt he4rt deleted a comment from coderabbitai Bot Jun 5, 2026
@he4rt he4rt deleted a comment from coderabbitai Bot Jun 5, 2026
@he4rt he4rt deleted a comment from coderabbitai Bot Jun 5, 2026
@he4rt he4rt deleted a comment from coderabbitai Bot Jun 5, 2026
@Clintonrocha98 Clintonrocha98 changed the title feat: comando de retrospectiva semanal da comunidade feat: ingestão de contribuições do GitHub e retrospectiva da comunidade Jun 5, 2026
@Clintonrocha98 Clintonrocha98 marked this pull request as draft June 5, 2026 13:19
O painel admin é multi-tenant, então tratar a allowlist e as contribuições
como globais quebrava ao paginar (o model não tinha relação `tenant`). Agora
cada comunidade tem o seu recorte, consistente com o resto do sistema.

- github_repositories e github_contributions carregam tenant_id; as uniques
  passam a (tenant_id, full_name) e (tenant_id, repo, type, external_ref).
- ProjectGithubEvent faz fan-out: uma entrega do webhook vira uma contribuição
  por tenant que acompanha o repo; o lake github_event_logs segue global.
- BackfillRepository::execute recebe o GithubRepository e carimba o tenant_id
  do repo de origem.
- A retrospectiva do portal filtra por tenant, resolvido por slug de rota
  (/comunidade/{tenant}/retrospectiva) com default em config('he4rt.main_tenant').
- GithubRepositoryResource volta a ser tenant-scoped; a validação de unicidade
  do form é escopada ao tenant atual.

Contribuição de repo compartilhado é duplicada por tenant — trade-off aceito em
favor do isolamento total entre comunidades.
@Clintonrocha98 Clintonrocha98 force-pushed the feat/community-retrospective branch from dda959b to fb4ee60 Compare June 6, 2026 00:59
@Clintonrocha98 Clintonrocha98 marked this pull request as ready for review June 6, 2026 14:31
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 6, 2026

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new Integration GitHub module: DB schemas, Eloquent models/factories, enums/DTOs, idempotent recorder and event, Saloon transport requests with PAT auth, a backfill service + CLI command with rate-limit handling, webhook signature middleware, webhook controller and projector, admin Filament resource and actions, portal retrospective read model, Livewire page and deck UI/styles, tests covering backfill/webhooks/retrospective, and docs/config entries including GITHUB_API_TOKEN and GITHUB_WEBHOOK_SECRET.

Possibly related PRs

  • he4rt/heartdevs.com#206: PanelServiceProvider wiring for Filament/panel configuration that overlaps with this PR’s PanelAdminServiceProvider changes.

Suggested reviewers

  • danielhe4rt
  • gvieira18

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 16

🧹 Nitpick comments (6)
app-modules/portal/src/Retrospective/CommunityRetrospective.php (1)

36-54: ⚡ Quick win

Push filterable clauses to the query layer.

The query loads all contributions by tenant and date range into memory, then filters in PHP. For repos, types, and person, these can be pushed to SQL to reduce memory usage and improve performance when the data set grows.

♻️ Proposed refactor to push filters to query
         /** `@var` Collection<int, GithubContribution> $contributions */
-        $contributions = GithubContribution::query()
+        $query = GithubContribution::query()
             ->where('tenant_id', $this->tenantId)
-            ->whereBetween('occurred_at', [$this->filters->since, $this->filters->until])
-            ->get()
+            ->whereBetween('occurred_at', [$this->filters->since, $this->filters->until]);
+
+        if ($this->filters->repos !== []) {
+            $query->whereIn('repo', $this->filters->repos);
+        }
+
+        if ($this->filters->types !== []) {
+            $query->whereIn('type', $this->filters->types);
+        }
+
+        if ($this->filters->person !== null) {
+            $query->where('actor_login', $this->filters->person);
+        }
+
+        $contributions = $query->get()
             ->when(
                 $this->filters->hideBots,
                 fn (Collection $items): Collection => $items->reject(fn (GithubContribution $contribution): bool => $this->isBot($contribution)),
             )
-            ->when(
-                $this->filters->repos !== [],
-                fn (Collection $items): Collection => $items->filter(fn (GithubContribution $contribution): bool => in_array($contribution->repo, $this->filters->repos, true)),
-            )
-            ->filter(fn (GithubContribution $contribution): bool => in_array($contribution->type, $this->filters->types, true))
             ->reject(fn (GithubContribution $contribution): bool => $this->filteredOutByOutcome($contribution))
-            ->when(
-                $this->filters->person !== null,
-                fn (Collection $items): Collection => $items->filter(fn (GithubContribution $contribution): bool => $contribution->actor_login === $this->filters->person),
-            )
             ->values();
app-modules/integration-github/tests/Feature/GithubRepositoryTest.php (1)

26-29: ⚡ Quick win

Remove implicit factory dependency in cross-tenant uniqueness coverage.

Line 27 and Line 28 assume separate tenants via factory defaults. Create explicit tenant A/B and attach each repository to keep this test deterministic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/integration-github/tests/Feature/GithubRepositoryTest.php` around
lines 26 - 29, The test "permite o mesmo full_name em tenants diferentes" relies
on implicit tenant separation from GithubRepository::factory()->create(), so
make it deterministic by explicitly creating two Tenant instances (e.g.,
$tenantA and $tenantB) and pass/associate each to the repository factory calls
(set tenant_id or use a factory state like forTenant) so the two
GithubRepository::factory()->create([...]) calls create records under different
tenants; update the test to attach the first create to $tenantA and the second
to $tenantB while keeping the same full_name.
app-modules/integration-github/tests/Feature/GithubContributionTest.php (1)

39-45: ⚡ Quick win

Make tenant separation explicit in this cross-tenant test.

Line 40 and Line 43 depend on factory defaults to create different tenants, which makes the test contract implicit and brittle. Bind each record to explicit tenants in this test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app-modules/integration-github/tests/Feature/GithubContributionTest.php`
around lines 39 - 45, The test creates two GithubContribution records relying on
factory defaults to produce different tenants; make tenant separation explicit
by creating or fetching two distinct tenant entities and passing them into each
factory call (e.g., set a tenant_id or associate a Tenant model) when calling
GithubContribution::factory()->create([...]) so the two creates reference
distinct tenants while keeping repo, type (ContributionType::Pr) and
external_ref the same.
app-modules/integration-github/database/migrations/2026_06_04_000001_create_github_repositories_table.php (1)

15-15: ⚡ Quick win

Clarify foreign key deletion behavior.

The tenant_id foreign key uses the default RESTRICT on delete. If a tenant is deleted, this will prevent deletion unless all their GitHub repositories are removed first. If cascade deletion is intended, add ->cascadeOnDelete(). If restrict is correct, consider ->restrictOnDelete() for explicitness.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app-modules/integration-github/database/migrations/2026_06_04_000001_create_github_repositories_table.php`
at line 15, The migration currently defines the foreign key as
$table->foreignUuid('tenant_id')->constrained('tenants') which leaves delete
behavior as the DB default (RESTRICT); update the migration in
create_github_repositories_table to make the intended behavior explicit: if
repositories should be removed when a tenant is deleted, change the constraint
to use ->cascadeOnDelete() on the tenant_id foreign key, otherwise append
->restrictOnDelete() to document the explicit restrict behavior.
app-modules/integration-github/database/migrations/2026_06_04_000002_create_github_contributions_table.php (1)

13-32: ⚖️ Poor tradeoff

Missing index for repository-scoped queries.

The retrospective filtering (per stack context) allows filtering by repository within a tenant. Queries like WHERE tenant_id = ? AND repo = ? ORDER BY occurred_at will perform a non-covering index scan. Add a composite index on (tenant_id, repo, occurred_at) to support efficient repo-scoped temporal queries.

📊 Proposed index addition
             $table->index(['tenant_id', 'occurred_at'], 'idx_github_contributions_tenant_time');
             $table->index('actor_id', 'idx_github_contributions_actor');
             $table->index(['tenant_id', 'type', 'occurred_at'], 'idx_github_contributions_type_time');
+            $table->index(['tenant_id', 'repo', 'occurred_at'], 'idx_github_contributions_repo_time');
         });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app-modules/integration-github/database/migrations/2026_06_04_000002_create_github_contributions_table.php`
around lines 13 - 32, The migration for the github_contributions table is
missing a composite index for repo-scoped temporal queries; inside the
Schema::create callback where indexes are added (the closure that defines
'github_contributions'), add a composite index on
['tenant_id','repo','occurred_at'] (e.g.
$table->index(['tenant_id','repo','occurred_at'],
'idx_github_contributions_tenant_repo_time')) next to the existing index calls
so queries with WHERE tenant_id = ? AND repo = ? ORDER BY occurred_at use the
index.
app-modules/portal/resources/views/components/retro/slides/highlights.blade.php (1)

2-2: 💤 Low value

Null array key access.

When $state is null, the expression [$state ?? ''] will attempt to access the array with an empty string key, which doesn't exist in the map. This will return the fallback 'var(--st-open)' correctly, but the logic is unclear. Consider using match($state) with explicit null handling or simplify to $stateColor = fn($state) => match($state) { 'merged' => 'var(--st-merged)', 'open' => 'var(--st-open)', 'closed' => 'var(--st-closed)', default => 'var(--st-open)' }.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app-modules/portal/resources/views/components/retro/slides/highlights.blade.php`
at line 2, The current $stateColor arrow closure uses an array lookup with a
potentially empty string key (defined as $state ?? ''), which is unclear and can
be simplified; update the $stateColor closure (named $stateColor) to use an
explicit match expression or a clear switch-style default so null/unknown states
map to 'var(--st-open)' — e.g. replace the array lookup with match($state) {
'merged' => 'var(--st-merged)', 'open' => 'var(--st-open)', 'closed' =>
'var(--st-closed)', default => 'var(--st-open)' } to make the logic explicit and
avoid null/empty-key access.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app-modules/integration-github/src/Backfill/BackfillRepository.php`:
- Around line 110-113: In backfillIssueComments, comments coming from PR
conversations are being attributed as issues because the call to upsert always
uses $this->refFromUrl($this->strv($comment, 'issue_url'), 'issue') and metadata
'kind' => 'issue'; change the logic to check $this->strv($comment,
'pull_request_url') first and if present use
$this->refFromUrl($this->strv($comment, 'pull_request_url'), 'pull_request')
(and set metadata 'kind' => 'pull_request'), otherwise fall back to using
'issue' with issue_url; update the upsert call in backfillIssueComments to pass
the chosen ref and kind while keeping other helpers ($this->strv, $this->userId,
$this->isBot, and ContributionType::Comment) unchanged.

In `@app-modules/integration-github/src/Console/BackfillGithubCommand.php`:
- Around line 52-59: The command currently logs per-repository exceptions in
BackfillGithubCommand (catch blocks using $exception and $throwable) but always
exits with success; modify the execute/handle flow to return a failure exit code
when any repository fails: introduce a boolean flag (e.g., $hadFailures =
false), set it to true inside both catch blocks where you call
$this->error(...), and after processing all repositories return
Symfony\Component\Console\Command\Command::FAILURE if $hadFailures is true
(otherwise return Command::SUCCESS). This ensures non-rate-limit errors cause
the command to exit with a non-zero status.

In `@app-modules/integration-github/src/Contributions/RecordContribution.php`:
- Around line 44-46: The handler currently dispatches GithubContributionRecorded
whenever $emit is true even for updates; change the emit condition to only fire
for newly-created contributions by checking the model creation flag returned by
updateOrCreate—use the returned $contribution->wasRecentlyCreated (or detect
creation before calling updateOrCreate) and only call event(new
GithubContributionRecorded($contribution)) when that flag is true to prevent
re-emitting on edits/replays.
- Around line 33-42: The event dispatch currently emits for both creates and
updates because you call GithubContribution::query()->updateOrCreate(...) and
then unconditionally dispatch when $emit is true; change the logic to only emit
when the model was newly created by checking the returned model's
wasRecentlyCreated property (i.e., after $contribution =
GithubContribution::query()->updateOrCreate(...), only dispatch
GithubContributionRecorded if $emit is true AND
$contribution->wasRecentlyCreated is true) so updates do not double-emit.

In `@app-modules/integration-github/src/Models/GithubContribution.php`:
- Around line 32-63: The GithubContribution model lacks mass-assignment
protection; add an explicit $fillable (or $guarded) property to the
GithubContribution class to whitelist safe attributes (e.g., 'tenant_id',
'type', 'actor_id', 'occurred_at', 'metadata') so calls like create() or
updateOrCreate() cannot set unintended fields; update the class definition near
the existing casts() and newFactory() methods to include the $fillable array (or
set protected $guarded = ['id','created_at','updated_at'] if you prefer a
blacklist approach).

In `@app-modules/integration-github/src/Models/GithubEventLog.php`:
- Around line 23-34: The GithubEventLog model lacks mass-assignment protection;
update the final class GithubEventLog (near the casts() method) to declare
explicit mass-assignment rules by adding either a $fillable array listing
allowed attributes (e.g., 'payload', 'event_type', etc.) or a $guarded array
(e.g., ['id'] or ['*'] as appropriate) to prevent uncontrolled create()/fill()
from assigning unintended attributes; ensure the chosen property is consistent
with other models in the codebase and placed on the class level.

In `@app-modules/integration-github/src/Models/GithubRepository.php`:
- Around line 27-32: The GithubRepository model lacks explicit mass assignment
protection; add either a $fillable array listing permitted attributes (e.g.,
name, owner, url, etc.) or set protected $guarded = [] on the GithubRepository
class to make intent explicit and avoid version-dependent behavior—update the
class definition around the GithubRepository declaration and ensure the chosen
property is declared as protected within the class.

In `@app-modules/integration-github/src/Webhook/GithubWebhookController.php`:
- Around line 19-27: The controller currently reads $event and $delivery and
immediately calls GithubEventLog::query()->firstOrCreate which can persist
invalid dedup keys; update GithubWebhookController to validate the required
GitHub headers ($event from 'X-GitHub-Event' and $delivery from
'X-GitHub-Delivery') before calling GithubEventLog::query()->firstOrCreate: if
$event is empty or $delivery is missing/empty, return an appropriate 4xx
response (or abort) and do not call firstOrCreate; only proceed to create or
dedupe the log when both headers are present and non-empty.

In `@app-modules/integration-github/src/Webhook/VerifyGithubSignature.php`:
- Around line 19-22: The current verification computes an HMAC even when $secret
may be an empty string, allowing forgable signatures; before computing $expected
use the VerifyGithubSignature logic to abort (e.g., abort or throw 403) if
$secret is null/empty/blank, then proceed to compute $expected =
'sha256='.hash_hmac('sha256', $request->getContent(), $secret) and use
hash_equals to compare; ensure the abort path references the same error/response
used for invalid signatures so blank secrets are rejected early.

In `@app-modules/panel-admin/src/Github/Resources/GithubRepositoryResource.php`:
- Around line 61-69: The full_name value must be normalized (lowercased and
trimmed) before persisting so owner/repo casing mismatches don't break
projection/allowlist matching; update the TextInput::make('full_name') form
field to canonicalize state using ->dehydrateStateUsing(fn($state) =>
strtolower(trim((string)$state))) or alternatively implement normalization on
the model (e.g., GithubRepository::setFullNameAttribute) or in
mutateFormDataBeforeSave so saved full_name is always lowercase and trimmed.

In
`@app-modules/portal/resources/views/components/retro/activity-chips.blade.php`:
- Line 11: The view renders $ref['url'] directly into an external href; update
the "activity-chips" Blade component to validate and allowlist URLs before
emitting href: parse the URL (parse_url or filter_var with FILTER_VALIDATE_URL),
ensure the scheme is http or https and the host is in an allowlist (or apply a
safe-host check), and only render href="{{ $ref['url'] }}" when those checks
pass; otherwise omit the href attribute or render a safe fallback; ensure you
still escape the value (e.g., e($url)) when inserting it.

In
`@app-modules/portal/resources/views/components/retro/composition-bar.blade.php`:
- Around line 6-10: The array building in the composition bar uses direct
accesses like $person['prs'], $person['reviews'], $person['issues'],
$person['comments'], and $person['commits'] which will error if a key is
missing; change each count access to use null-coalescing (e.g. $person['prs'] ??
0) so missing keys default to 0 and keep the rest of the array entries (the
color and label fields) unchanged.

In `@app-modules/portal/resources/views/components/retro/deck.blade.php`:
- Around line 64-71: The global `@keydown.window` handler on the component
unconditionally calls $event.preventDefault() for ArrowLeft/Right, which
prevents normal caret/selection behavior when focus is inside form controls;
update the handler in the deck blade so it first checks document.activeElement
(e.g., tagName IN ['INPUT','TEXTAREA','SELECT'] or element.isContentEditable)
and returns early without calling go(...) or preventDefault() if focus is inside
a form control or an editable element, otherwise proceed to call go(active +/-
1) and then preventDefault(); modify the handler surrounding the go and
preventDefault calls to implement this conditional.
- Around line 92-101: The dot and arrow buttons lack accessible names; update
the template x-for button.dot and the nav buttons (class="navbtn") to include
descriptive ARIA attributes: for the dots add a dynamic aria-label like "Go to
slide {i}" and set aria-current (or aria-pressed) when active (use active and i
to compute state) and for the prev/next navbtns add static aria-labels "Previous
slide" and "Next slide" and ensure the :disabled state is reflected with
aria-disabled when appropriate; keep using the existing go(...) handler and the
active/total variables.

In `@app-modules/portal/resources/views/components/retro/filters.blade.php`:
- Around line 37-40: Replace pointer-only spans/divs used as interactive
controls with real interactive elements or make them keyboard-operable: change
the <span class="chip" wire:click="setPreset('...')"> controls to <button
type="button" class="chip" wire:click="setPreset('...')"> for the presets (and
similarly convert other clickable spans/divs in the same file), or if you must
keep non-button elements add role="button" tabindex="0" and a keydown handler
that invokes the same action on Enter/Space. Ensure you update all occurrences
referenced in the comment (the controls that call setPreset via wire:click and
the other clickable chips/toggles) so keyboard users can focus and activate them
and existing styling/ARIA remains intact.

In `@app-modules/portal/resources/views/components/retro/person-card.blade.php`:
- Around line 5-25: The template is reading $person['url'], $person['avatar'],
$person['login'], and $person['total'] directly which will emit warnings if keys
are missing; update person-card.blade.php to defensively access these keys (e.g.
use null-coalescing or data_get) so each usage falls back to a safe default:
href should use ($person['url'] ?? '#'), avatar src should use
($person['avatar'] ?? 'default-avatar.png') and keep the onerror fallback
referencing ($person['login'] ?? 'unknown'), alt should use ($person['login'] ??
'unknown'), and total should default to 0 for the interaction text (use
($person['total'] ?? 0) and apply the singular/plural logic against that value).
Ensure all occurrences of $person['url'], $person['avatar'], $person['login'],
and $person['total'] are updated consistently.

---

Nitpick comments:
In
`@app-modules/integration-github/database/migrations/2026_06_04_000001_create_github_repositories_table.php`:
- Line 15: The migration currently defines the foreign key as
$table->foreignUuid('tenant_id')->constrained('tenants') which leaves delete
behavior as the DB default (RESTRICT); update the migration in
create_github_repositories_table to make the intended behavior explicit: if
repositories should be removed when a tenant is deleted, change the constraint
to use ->cascadeOnDelete() on the tenant_id foreign key, otherwise append
->restrictOnDelete() to document the explicit restrict behavior.

In
`@app-modules/integration-github/database/migrations/2026_06_04_000002_create_github_contributions_table.php`:
- Around line 13-32: The migration for the github_contributions table is missing
a composite index for repo-scoped temporal queries; inside the Schema::create
callback where indexes are added (the closure that defines
'github_contributions'), add a composite index on
['tenant_id','repo','occurred_at'] (e.g.
$table->index(['tenant_id','repo','occurred_at'],
'idx_github_contributions_tenant_repo_time')) next to the existing index calls
so queries with WHERE tenant_id = ? AND repo = ? ORDER BY occurred_at use the
index.

In `@app-modules/integration-github/tests/Feature/GithubContributionTest.php`:
- Around line 39-45: The test creates two GithubContribution records relying on
factory defaults to produce different tenants; make tenant separation explicit
by creating or fetching two distinct tenant entities and passing them into each
factory call (e.g., set a tenant_id or associate a Tenant model) when calling
GithubContribution::factory()->create([...]) so the two creates reference
distinct tenants while keeping repo, type (ContributionType::Pr) and
external_ref the same.

In `@app-modules/integration-github/tests/Feature/GithubRepositoryTest.php`:
- Around line 26-29: The test "permite o mesmo full_name em tenants diferentes"
relies on implicit tenant separation from GithubRepository::factory()->create(),
so make it deterministic by explicitly creating two Tenant instances (e.g.,
$tenantA and $tenantB) and pass/associate each to the repository factory calls
(set tenant_id or use a factory state like forTenant) so the two
GithubRepository::factory()->create([...]) calls create records under different
tenants; update the test to attach the first create to $tenantA and the second
to $tenantB while keeping the same full_name.

In
`@app-modules/portal/resources/views/components/retro/slides/highlights.blade.php`:
- Line 2: The current $stateColor arrow closure uses an array lookup with a
potentially empty string key (defined as $state ?? ''), which is unclear and can
be simplified; update the $stateColor closure (named $stateColor) to use an
explicit match expression or a clear switch-style default so null/unknown states
map to 'var(--st-open)' — e.g. replace the array lookup with match($state) {
'merged' => 'var(--st-merged)', 'open' => 'var(--st-open)', 'closed' =>
'var(--st-closed)', default => 'var(--st-open)' } to make the logic explicit and
avoid null/empty-key access.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository YAML (base), Central YAML (inherited)

Review profile: CHILL

Plan: Pro

Run ID: 6277e329-9faa-4414-a723-b904b39c1ff3

📥 Commits

Reviewing files that changed from the base of the PR and between dda959b and 377f182.

⛔ Files ignored due to path filters (1)
  • public/images/retro-heart.png is excluded by !**/*.png
📒 Files selected for processing (71)
  • .env.example
  • CONTEXT-MAP.md
  • app-modules/integration-github/CONTEXT.md
  • app-modules/integration-github/database/factories/GithubContributionFactory.php
  • app-modules/integration-github/database/factories/GithubRepositoryFactory.php
  • app-modules/integration-github/database/migrations/2026_06_04_000001_create_github_repositories_table.php
  • app-modules/integration-github/database/migrations/2026_06_04_000002_create_github_contributions_table.php
  • app-modules/integration-github/database/migrations/2026_06_04_000003_create_github_event_logs_table.php
  • app-modules/integration-github/docs/adr/0001-github-community-contributions.md
  • app-modules/integration-github/routes/github-webhook-routes.php
  • app-modules/integration-github/src/Backfill/BackfillRepository.php
  • app-modules/integration-github/src/Backfill/RateLimit.php
  • app-modules/integration-github/src/Console/BackfillGithubCommand.php
  • app-modules/integration-github/src/Contributions/RecordContribution.php
  • app-modules/integration-github/src/Enums/ContributionType.php
  • app-modules/integration-github/src/Events/GithubContributionRecorded.php
  • app-modules/integration-github/src/IntegrationGithubServiceProvider.php
  • app-modules/integration-github/src/Models/GithubContribution.php
  • app-modules/integration-github/src/Models/GithubEventLog.php
  • app-modules/integration-github/src/Models/GithubRepository.php
  • app-modules/integration-github/src/Transport/GitHubApiConnector.php
  • app-modules/integration-github/src/Transport/Requests/Contributions/GetPullRequest.php
  • app-modules/integration-github/src/Transport/Requests/Contributions/ListCommits.php
  • app-modules/integration-github/src/Transport/Requests/Contributions/ListIssueComments.php
  • app-modules/integration-github/src/Transport/Requests/Contributions/ListIssues.php
  • app-modules/integration-github/src/Transport/Requests/Contributions/ListPullRequestReviewComments.php
  • app-modules/integration-github/src/Transport/Requests/Contributions/ListPullRequestReviews.php
  • app-modules/integration-github/src/Transport/Requests/Contributions/ListPullRequests.php
  • app-modules/integration-github/src/Webhook/GithubWebhookController.php
  • app-modules/integration-github/src/Webhook/ProjectGithubEvent.php
  • app-modules/integration-github/src/Webhook/VerifyGithubSignature.php
  • app-modules/integration-github/tests/Feature/BackfillGithubCommandTest.php
  • app-modules/integration-github/tests/Feature/BackfillRepositoryTest.php
  • app-modules/integration-github/tests/Feature/GithubContributionTest.php
  • app-modules/integration-github/tests/Feature/GithubRepositoryTest.php
  • app-modules/integration-github/tests/Feature/GithubWebhookTest.php
  • app-modules/panel-admin/src/Github/GithubCluster.php
  • app-modules/panel-admin/src/Github/Resources/GithubRepositoryResource.php
  • app-modules/panel-admin/src/Github/Resources/GithubRepositoryResource/Pages/CreateGithubRepository.php
  • app-modules/panel-admin/src/Github/Resources/GithubRepositoryResource/Pages/EditGithubRepository.php
  • app-modules/panel-admin/src/Github/Resources/GithubRepositoryResource/Pages/ListGithubRepositories.php
  • app-modules/panel-admin/src/PanelAdminServiceProvider.php
  • app-modules/panel-admin/tests/Feature/Github/GithubRepositoryResourceTest.php
  • app-modules/portal/resources/css/retrospective.css
  • app-modules/portal/resources/views/community-retrospective.blade.php
  • app-modules/portal/resources/views/components/layouts/deck.blade.php
  • app-modules/portal/resources/views/components/retro/activity-chips.blade.php
  • app-modules/portal/resources/views/components/retro/badges.blade.php
  • app-modules/portal/resources/views/components/retro/composition-bar.blade.php
  • app-modules/portal/resources/views/components/retro/deck.blade.php
  • app-modules/portal/resources/views/components/retro/filters.blade.php
  • app-modules/portal/resources/views/components/retro/person-card.blade.php
  • app-modules/portal/resources/views/components/retro/pr-row.blade.php
  • app-modules/portal/resources/views/components/retro/slides/closing.blade.php
  • app-modules/portal/resources/views/components/retro/slides/community.blade.php
  • app-modules/portal/resources/views/components/retro/slides/core.blade.php
  • app-modules/portal/resources/views/components/retro/slides/cover.blade.php
  • app-modules/portal/resources/views/components/retro/slides/highlights.blade.php
  • app-modules/portal/resources/views/components/retro/slides/panorama.blade.php
  • app-modules/portal/resources/views/components/retro/slides/repo.blade.php
  • app-modules/portal/src/Livewire/CommunityRetrospectivePage.php
  • app-modules/portal/src/PortalServiceProvider.php
  • app-modules/portal/src/Retrospective/CommunityRetrospective.php
  • app-modules/portal/src/Retrospective/RetrospectiveFilters.php
  • app-modules/portal/tests/Feature/CommunityRetrospectivePageTest.php
  • app-modules/portal/tests/Feature/CommunityRetrospectiveTest.php
  • app-modules/portal/tests/Feature/RetrospectiveFiltersTest.php
  • config/he4rt.php
  • config/services.php
  • docs/plans/2026-06-04-github-community-contributions.md
  • vite.config.js
✅ Files skipped from review due to trivial changes (11)
  • app-modules/integration-github/src/Enums/ContributionType.php
  • app-modules/panel-admin/src/Github/Resources/GithubRepositoryResource/Pages/ListGithubRepositories.php
  • app-modules/portal/resources/views/components/retro/slides/cover.blade.php
  • app-modules/integration-github/src/Transport/Requests/Contributions/GetPullRequest.php
  • app-modules/portal/resources/views/components/retro/slides/closing.blade.php
  • config/he4rt.php
  • vite.config.js
  • app-modules/integration-github/src/Transport/Requests/Contributions/ListPullRequestReviewComments.php
  • app-modules/panel-admin/src/Github/GithubCluster.php
  • .env.example
  • CONTEXT-MAP.md

Comment thread app-modules/integration-github/src/Backfill/BackfillRepository.php Outdated
Comment thread app-modules/integration-github/src/Console/BackfillGithubCommand.php Outdated
Comment thread app-modules/integration-github/src/Contributions/RecordContribution.php Outdated
Comment thread app-modules/integration-github/src/Contributions/RecordContribution.php Outdated
Comment thread app-modules/integration-github/src/Models/GithubContribution.php
Comment thread app-modules/portal/resources/views/components/retro/deck.blade.php
Comment thread app-modules/portal/resources/views/components/retro/deck.blade.php Outdated
Clintonrocha98 and others added 2 commits June 6, 2026 12:21
… repo, seam)

- VerifyGithubSignature recusa (500) quando o secret não está configurado,
  fechando o vetor de webhook forjável com HMAC de chave vazia
- full_name canonicalizado em minúsculas (mutator) e o repo do payload
  normalizado no ProjectGithubEvent, evitando perda silenciosa de eventos
  e fragmentação por diferença de case
- GithubContributionRecorded só dispara na criação (wasRecentlyCreated),
  evitando re-emissão da seam em edições/replays do webhook
…ReviewComment

- escritor único (RecordContribution) recebe um DTO; remove o proxy upsert()
- separa review comments em um case próprio do enum (o enum constrói o ref)
- helpers coesos (stringFrom/intFrom/actorLogin/actorId) + Str:: no lugar de mb_/str_ends_with/preg_match
- ALL_TYPES derivado do enum (remove duplicação)
- deck: cor/chip/fatia da DNA para review comments
@danielhe4rt
Copy link
Copy Markdown
Contributor

🔐 Configuração de produção — token, scopes e ENVs

Pra ligar a ingestão do GitHub em produção precisa de 2 segredos (+1 opcional) e registrar o webhook na org. Sem isso, o backfill pelo painel ainda funciona (autenticado pelo token); só o tempo real (webhook) fica desligado.

1. GITHUB_API_TOKEN — backfill (REST)

Usado pelo GitHubApiConnector como Authorization: Bearer <token> para puxar PRs, reviews, issues, comentários e commits dos repos da allowlist. Sem ele as requisições vão sem auth e o GitHub limita a 60 req/h (estoura rate limit rápido); com ele, 5.000 req/h.

Onde gerar:

  • Fine-grained (recomendado — read-only): Settings → Developer settings → Personal access tokens → Fine-grained tokenshttps://github.com/settings/tokens?type=beta
    • Resource owner: a org he4rt (pode exigir aprovação da org).
    • Repository access: os repositórios públicos que entram na allowlist (ou "All repositories").
    • Permissions (todas Read-only):
      • Metadata: Read (obrigatório)
      • Pull requests: Read
      • Issues: Read
      • Contents: Read (endpoint de commits)
  • Classic (alternativa): https://github.com/settings/tokens → escopo public_repo. Use repo apenas se algum dia rastrear repositório privado.

2. GITHUB_WEBHOOK_SECRET — tempo real (webhook)

HMAC X-Hub-Signature-256 verificado pelo middleware VerifyGithubSignature. Fail-safe: se estiver vazio, o webhook é rejeitado com HTTP 500 e nada é gravado.

Gerar um segredo forte:

openssl rand -hex 32

Use o mesmo valor no .env e na config do webhook (passo 4).

3. HE4RT_MAIN_TENANT (opcional, default he4rt)

Tenant padrão da rota pública /comunidade/retrospectiva quando nenhum slug é informado. Só preencha se o slug do tenant principal for diferente de he4rt.

ENVs (já presentes no .env.example, linhas 109–110)

GITHUB_API_TOKEN=github_pat_xxx     # fine-grained (ou ghp_xxx classic)
GITHUB_WEBHOOK_SECRET=xxx           # mesmo valor configurado no webhook
# HE4RT_MAIN_TENANT=he4rt           # opcional

⚠️ Não confundir com o OAuth de login (GITHUB_OAUTH_CLIENT_ID / GITHUB_OAUTH_CLIENT_SECRET) — é outra configuração e não é necessária para esta feature.

4. Registrar o webhook (org ou repositório)

Org he4rt → Settings → Webhooks → Add webhook:

  • Payload URL: https://<dominio>/api/webhooks/github
  • Content type: application/json
  • Secret: o valor de GITHUB_WEBHOOK_SECRET
  • Events: "Let me select individual events"
    pull_request, pull_request_review, issues, issue_comment, pull_request_review_comment, push
  • Active:

Depois de configurar

  1. Cadastrar os repos no painel (admin → cluster GitHub → Repositórios) e clicar "Backfill agora" (ou php artisan github:backfill owner/repo).
  2. Rodar npm run build (entrypoint retrospective.css do deck).

Reviews PENDING (rascunho não enviado) não têm submitted_at — a API do GitHub
lista o rascunho do próprio usuário do token. Gravar occurred_at = "" estourava
um QueryException (invalid input syntax for type timestamp) e abortava o backfill.
Agora o backfill e o webhook pulam esses reviews. Bug latente, exposto por dados reais.
O backfill faz centenas de requests REST e estourava o timeout no request HTTP
do Livewire (cURL error 28). Agora roda em segundo plano.

- BackfillGithubRepository (ShouldQueue, ShouldBeUniqueUntilProcessing): executa o
  backfill no worker; resumível por idempotência; em rate limit faz release() até o
  reset (não conta como falha); retryUntil 6h + maxExceptions 3 + timeout 600s.
- painel "Backfill agora" agora ENFILEIRA o job e volta na hora (notifica "enfileirado").
- RateLimit::secondsUntilReset() para re-agendar o job até o reset.
- comando github:backfill segue síncrono (CLI não tem timeout).

Requer fila assíncrona + worker (prod=redis). Testes: job (release/stamp) + painel (dispatch).
…CLI)

- BackfillRepository::execute ganha um callback opcional de progresso, chamado por
  contribuição gravada com o ContributionType (seam agnóstico de UI). Job e painel
  passam null → comportamento idêntico.
- github:backfill refinado com Laravel Prompts (intro/table/outro/warning) + barra
  de progresso ao vivo por repo, com contador por tipo (PRs · reviews · issues · …).
- testes: callback chamado por contribuição (na ordem); comando segue cobrindo
  rate-limit (FAILURE + sem stamp).
Banco zerado / nenhuma contribuição ingerida mostrava 3 slides com tudo "0".
Agora, quando não há contribuição alguma para o tenant (count(repoOptions) === 0),
o deck renderiza um único slide: coração + "Métricas indisponíveis" + CTA pra
reunião semanal (discord.gg/he4rt). O deck entra em modo `bare` (sem progress,
FAB de filtros nem navbar). Gatilho é "sem dado nenhum" (não a janela), então uma
semana parada com histórico segue caindo no deck normal com filtros.
Por padrão o backfill agora é incremental: com last_backfilled_at preenchido, só
busca o que mudou desde (last_backfilled_at − 1 dia). Sem ele (1ª run) ou com
--full, varre o histórico inteiro. Mata a varredura completa toda vez.

- /issues, /issues/comments, /pulls/comments, /commits → query `since` (server-side)
- /pulls (a API não tem `since`) → sort=updated&direction=desc + early-stop no
  paginate quando updated_at < corte; só os PRs recentes fazem GetPullRequest+reviews
- execute(repository, onProgress, full=false); comando ganha --full; painel/job
  ficam incrementais por padrão (1º backfill é completo, cliques seguintes rápidos)
- params `since` validados na doc oficial do GitHub via context7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants